﻿/*
 * This file is part of MonoStrategy.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://monostrategy.codeplex.com/
 */
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using OpenTK;
using OpenTK.Input;

using System.Runtime.InteropServices;
using System.Threading;

#if EMBEDDED
    using OpenTK.Graphics.ES20;
#else
using OpenTK.Graphics.OpenGL;
#endif

namespace MonoStrategy.RenderSystem
{
    [ObfuscationAttribute(Feature = "renaming", ApplyToMembers = true)]
    public class GLRenderConfig
    {
        public int ViewportWidth { get; set; }
        public int ViewportHeight { get; set; }
        public bool IsFullScreen { get; set; }

        public GLRenderConfig()
        {
            ViewportWidth = 800;
            ViewportHeight = 600;
            IsFullScreen = false;
        }
    }

    public partial class GLRenderer 
    {

        /// <summary>
        /// One time initialization of a new GL context. Must be called from within the render thread!
        /// </summary>
        /// <param name="inPixelWidth">Pixel width of the viewport.</param>
        /// <param name="inPixelHeight">Pixel height of the viewport.</param>
        private void InitializeGLContext(int inPixelWidth, int inPixelHeight)
        {
            // just dump some system information into log file which will be important for bugreports.
            String logEntry = "Creating OpenGL Renderer...\r\n" +
                "    => OpenGL Version: \"" + GL.GetString(StringName.Version) + "\"\r\n" +
                "    => GLSL Version: \"" + GL.GetString(StringName.ShadingLanguageVersion) + "\"\r\n" +
                "    => OpenGL Vendor: \"" + GL.GetString(StringName.Vendor) + "\"\r\n" +
                "    => OpenGL Renderer: \"" + GL.GetString(StringName.Renderer) + "\"\r\n" +
                "    => OpenGL Extensions: \"\r\n        " + GL.GetString(StringName.Extensions).Replace(" ", "\r\n        ") + "\"\r\n";

            Log.LogMessage(logEntry);

            GL.ClearColor(Color.DarkGreen);
            GL.ClearDepth(1.0f);
            GL.DepthFunc(DepthFunction.Lequal);

            // load shaders
            m_2DSceneProgram = new GLProgram(
                new GLShader(Program.GetResourcePath("Shaders/2DScene.vert")), 
                new GLShader(Program.GetResourcePath("Shaders/2DScene.frag")), true);

            UpdateViewport(inPixelWidth, inPixelHeight);
        }

        /// <summary>
        /// Should be called whenever the surface changes its size and properly
        /// updates the GL viewport and other related properties.
        /// </summary>
        /// <param name="inPixelWidth">Pixel width of the viewport.</param>
        /// <param name="inPixelHeight">Pixel height of the viewport.</param>
        private void UpdateViewport(int inPixelWidth, int inPixelHeight)
        {
            if ((inPixelWidth <= 0) || (inPixelHeight <= 0))
                return;

            GL.Viewport(0, 0, inPixelWidth, inPixelHeight);

            ViewportWidth = inPixelWidth;
            ViewportHeight = inPixelHeight;
            AspectRatio = ((Double)inPixelWidth / inPixelHeight);

            if (TerrainRenderer != null)
                TerrainRenderer.UpdateViewport();
        }

        private GLRenderer()
        {
            m_Watch = new System.Diagnostics.Stopwatch();
            m_Watch.Start();
            CurrentTextureIDs = new int[16];
        }

        public GLRenderer(GLRenderConfig inConfig) : this()
        {
            if (inConfig == null)
                throw new ArgumentNullException();

            RenderThread = new System.Threading.Thread(() => 
            {
                try
                {
                    var m_Window = new GameWindow(
                        inConfig.ViewportWidth,
                        inConfig.ViewportHeight,
                        OpenTK.Graphics.GraphicsMode.Default,
                        "MonoStrategy MILESTONE 0",
                        inConfig.IsFullScreen ? GameWindowFlags.Fullscreen : GameWindowFlags.Default);

                    m_Window.Visible = true;

                    InitializeGLContext(inConfig.ViewportWidth, inConfig.ViewportHeight);

                    m_Window.Mouse.Move += RaiseMouseMove;
                    m_Window.Mouse.ButtonDown += RaiseMouseButtonDown;
                    m_Window.Mouse.ButtonUp += RaiseMouseButtonUp;
                    m_Window.Keyboard.KeyDown += RaiseKeyboardKeyDown;
                    m_Window.Keyboard.KeyUp += RaiseKeyboardKeyUp;
                    m_Window.Resize += (unused, args) => { UpdateViewport(m_Window.Width, m_Window.Height); };

                    var watch = new System.Diagnostics.Stopwatch();
                    long avgMillis = 0, frameCount = 0;

                    while (true)
                    {
                        m_Window.ProcessEvents();

                        if (m_Window.IsExiting)
                            return;

                        RaiseKeyboardKeyRepeat();

                        watch.Start();
                        {
                            RunPerFrameTasks();
                            
                            m_Window.SwapBuffers();
                        }
                        watch.Stop();

                        frameCount++;
                        avgMillis += watch.ElapsedMilliseconds;

                        watch.Reset();

                        if (frameCount > 30)
                        {
                            float fps = (1000 / (avgMillis / (float)frameCount));

                            if (TerrainRenderer != null)
                            {
                                m_Window.Title = String.Format("MonoStrategy MILESTONE 0 (FPS: {0:0.##}; Screen: ({1:0.##}:{2:0.##}); Bounds: ({3}); Mouse: ({4}); PPM: {5}ms [max: {6}ms])",
                                    fps, TerrainRenderer.ScreenXY.X, 
                                    TerrainRenderer.ScreenXY.Y, 
                                    TerrainRenderer.ScreenBounds, 
                                    TerrainRenderer.GridXY, 
                                    (TerrainRenderer.Terrain.Map != null) ? TerrainRenderer.Terrain.Map.AvgPlanMillis : 0,
                                    (TerrainRenderer.Terrain.Map != null) ? TerrainRenderer.Terrain.Map.MaxPlanMillis : 0);
                            }

                            frameCount = 0;
                            avgMillis = 0;
                        }

                        System.Threading.Thread.Sleep(20);
                    }
                }
                catch(Exception e)
                {
                    Log.LogExceptionCritical(e);
                }
                finally
                {
                    IsTerminated = true;

                    Dispose();
                }
            });

            RenderThread.IsBackground = true;
            RenderThread.Start();
        }

        private void RunPerFrameTasks()
        {
            if ((Terrain != null) && (TerrainRenderer == null))
            {
                TerrainRenderer = new TerrainRenderer(this, Terrain);
            }

            GL.ClearColor(Color.Black);
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

            if ((TerrainRenderer != null) && EnableTerrainRendering)
                TerrainRenderer.OnRender();

            // render GUI sprites
            Matrix4 spriteMat = new Matrix4(
                2, 0, 0, 0,
                0, -2, 0, 0,
                0, 0, -1, 0,
                -1, 1, 0, 1
                );

            m_2DSceneProgram.Bind(spriteMat, Matrix4.Identity, Matrix4.Identity);

            GL.BlendFunc(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha);
            GL.Enable(EnableCap.Blend);
            GL.Disable(EnableCap.DepthTest);
            {
                if (OnRenderSprites != null)
                    OnRenderSprites(this);
            }
            GL.Disable(EnableCap.Blend);
        }

        public void WaitForTermination()
        {
            while (!IsTerminated)
            {
                Thread.Sleep(500);
            }
        }

        /// <summary>
        /// This is only intended for AnimationEditor and provides limited functionality!
        /// </summary>
        /// <returns></returns>
        public static GLRenderer CreateControl(System.Windows.Forms.ContainerControl inParent)
        {
            GLRenderer result = new GLRenderer();
            GLControl glCtrl = new GLControl();
            System.Windows.Forms.Timer repaintTimer = new System.Windows.Forms.Timer();
            repaintTimer.Interval = 30;
            repaintTimer.Tick += (sender, args) =>
            {
                glCtrl.Invalidate();
            };

            bool isInitialized = false;

            glCtrl.Dock = System.Windows.Forms.DockStyle.Fill;

            // also update viewport on resize
            glCtrl.SizeChanged += (sender, args) =>
            {
                result.UpdateViewport(glCtrl.Width, glCtrl.Height);
            };

            // is invoked whenever the control is to be repaint
            glCtrl.Paint += (sender, args) =>
            {
                try
                {
                    // stopping the timer prevents CPU freeze, if rendering takes longer than desired frame rate.
                    repaintTimer.Stop();

                    if (!isInitialized)
                    {
                        result.InitializeGLContext(glCtrl.Width, glCtrl.Height);

                        isInitialized = true;
                    }

                    result.RunPerFrameTasks();

                    glCtrl.SwapBuffers();
                }
                catch (Exception e)
                {
                    Log.LogExceptionCritical(e);
                }
                finally
                {
                    repaintTimer.Start();
                }
            };
            glCtrl.MouseMove += (sender, args) =>
            {
                result.RaiseMouseMove(sender, new MouseMoveEventArgs(args.X, args.Y, 0,0));
            };
            glCtrl.MouseDown += (sender, args) =>
            {
                MouseButton btn;

                switch (args.Button)
                {
                    case System.Windows.Forms.MouseButtons.Left: btn = MouseButton.Left; break;
                    case System.Windows.Forms.MouseButtons.Right: btn = MouseButton.Right; break;
                    case System.Windows.Forms.MouseButtons.Middle: btn = MouseButton.Middle; break;
                    default: return;
                }

                result.RaiseMouseButtonDown(sender, new MouseButtonEventArgs(args.X, args.Y, btn, true));
            };
            glCtrl.MouseUp += (sender, args) =>
            {
                MouseButton btn;

                switch (args.Button)
                {
                    case System.Windows.Forms.MouseButtons.Left: btn = MouseButton.Left; break;
                    case System.Windows.Forms.MouseButtons.Right: btn = MouseButton.Right; break;
                    case System.Windows.Forms.MouseButtons.Middle: btn = MouseButton.Middle; break;
                    default: return;
                }

                result.RaiseMouseButtonUp(sender, new MouseButtonEventArgs(args.X, args.Y, btn, false));
            };

            result.RenderThread = Thread.CurrentThread;

            repaintTimer.Start();
            
            // finally attach the control to its parent
            inParent.Controls.Add(glCtrl);

            return result;
        }

        /// <summary>
        /// In case of <see cref="CreateAsWindow"/> this method is called automatically on exit.
        /// In case of <see cref="CreateInControl"/> you have to call it yourself when the renderer
        /// is no longer needed.
        /// </summary>
        public void Dispose()
        {
            foreach (var image in m_RegisteredImages)
            {
                if ((image != null) && (image.Texture != null))
                    image.Texture.Dispose();
            }

            m_RegisteredImages.Clear();

            // release shaders
            if (m_2DSceneProgram != null)
                m_2DSceneProgram.Dispose();

            m_2DSceneProgram = null;
        }
    }
}
